package search;

import java.io.IOException;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map.Entry;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;

import model.Doc;
import model.Node;
import model.Term;

import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;

import repco.RepCo;
import sparqlclient.SparqlRequest;

import utils.Utils;
import utils.stemmer.Stemmer;
import database.DBConnector;
import database.DBNames;

public class Search
{
	private String request;
	private DBConnector dc;
	
	// Hashmap associant un poids aux noeuds
	private ArrayList<HashMap<Integer, Double>> nodeWeights;
	private HashMap<Integer, Double> globalNodeWeight;
			
	// Hashmap pour indexer document et noeuds par leur ID
	private HashMap<Integer, Doc> docs;
	private HashMap<Integer, Node> nodes;
	
	// Noeuds tries
	private ArrayList<Node> sortedNodes;
	
	// Poids de la recherche (pour les extensions par ontologie)
	private double weight;
	
	
	public Search(String request, DBConnector dc)
	{
		this.request = request;
		this.dc = dc;
		this.dc.connect();
		this.sortedNodes = new ArrayList<>();
		this.docs = new HashMap<>();
		this.nodes = new HashMap<>();
		this.nodeWeights = new ArrayList<>();
		this.globalNodeWeight= new HashMap<>(); 
		this.weight = 1.;
	}
	
	public Search(String request, double weight, DBConnector dc)
	{
		this.request = request;
		this.dc = dc;
		this.dc.connect();
		this.sortedNodes = new ArrayList<>();
		this.docs = new HashMap<>();
		this.nodes = new HashMap<>();
		this.nodeWeights = new ArrayList<>();
		this.globalNodeWeight = new HashMap<>(); 
		this.weight = weight;
		search();
	}
	
	public void getDBData()
	{
		// Traduction de la requete utilisateur en liste de mots interpretables par le moteur
		ArrayList<String> requestTerms = Utils.parseRequest(this.request);
		
		// Requete DB pour recuperer les objets associes aux termes de la recherches
		StringBuilder termRequest = new StringBuilder("SELECT * FROM " + DBNames.TERM + " WHERE " + DBNames.TERM_VALUE + " IN ");
		termRequest.append( Utils.listToIn(requestTerms)); termRequest.append(";");
		ResultSet rsTerm = this.dc.executeQuery(termRequest.toString());
		
		try
		{
			if (rsTerm.first())
			{
				// Pour chaque mot de la recherche
				while (!rsTerm.isAfterLast())
				{
					// Creation de l'objet associe en utilisant les donnees de la BD
					Term term = new Term(rsTerm);
					
					// Enregistrement des poids par noeud
					nodeWeights.add(term.getWeight());
	
					// Determination de tous les differents noeuds dans lesquels on trouve le terme actuel
					for (Integer node : term.getPositions().keySet())
					{
						if (!nodes.containsKey(node))
							nodes.put(node, null);
					}
					
					// Passage au resultat suivant de la requete
					rsTerm.next();
				}
			}
			
			// Requete DB pour recup tous les noeuds trouves : id,type, weight, xpath et
			// fdoc d'un noeud et pour recuperer le nom du fichier associe
			StringBuilder nodeRequest = new StringBuilder("SELECT * FROM " + DBNames.NODE + " WHERE " + DBNames.NODE_ID + " IN ");
			nodeRequest.append(Utils.listToIn(this.nodes.keySet())); nodeRequest.append(";");
			
			ResultSet rsNode = this.dc.executeQuery(nodeRequest.toString());
			
			if (rsNode.first())
			{
				while (!rsNode.isAfterLast())
				{
					// Recherche des noeuds dans la BD
					Node n = new Node(rsNode);
		
					// Ajout du document si le noeud est un paragraphe
					if (n.isP())
					{
						this.nodes.put(n.getID(), n);
						this.docs.put(n.getFdoc(), null);
					}
					else
					{
						this.nodes.remove(n.getID());
					}
					
					// passage au resultat suivant de la requete
					rsNode.next();
				}
			}
			
			// Recuperation des documents
			StringBuilder docRequest = new StringBuilder("SELECT * FROM " + DBNames.DOCUMENT + " WHERE " + DBNames.DOC_ID + " IN ");
			docRequest.append(Utils.listToIn(docs.keySet())); docRequest.append(";");
			ResultSet rsDoc = this.dc.executeQuery(docRequest.toString());
			
			if (rsDoc.first())
			{
				while (!rsDoc.isAfterLast())
				{
					Doc doc = new Doc(rsDoc);
					docs.put(doc.getID(), doc);
					rsDoc.next();
				}
			}
			
		} catch (SQLException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	
	
	public void computeWeights()
	{
		// Hashmap associant un poids aux noeuds
		HashMap<Integer, Double> globalNodeWeight = new HashMap<>();
		
		// Parcours de AL contenant tous les noeuds impliques dans la requete
		for (Node node : this.nodes.values())
		{
			int in = 0;
			double weight = 1.;
			for (HashMap<Integer, Double> w : nodeWeights)
			{
				if (w.containsKey(node.getID()))
				{
					weight += w.get(node.getID());
					in++;
				}
			}
			
			weight *= in;
			weight *= node.getWeight();
			
			globalNodeWeight.put(node.getID(), weight);
		}
		
		this.globalNodeWeight = globalNodeWeight;
	}
	
	public void sortNodes()
	{
		this.sortedNodes.clear();
		
		// Tri de globalNodeWeght par poids decroissant
		// Ajout des entrees de la map a une liste
		final ArrayList<Entry<Integer, Double>> entries = new ArrayList<Entry<Integer, Double>>(this.globalNodeWeight.entrySet());

		// Tri de la liste sur la valeur de l'entree
		Collections.sort(entries, new Comparator<Entry<Integer, Double>>()
		{
			@Override
			public int compare(Entry<Integer, Double> o1,
					Entry<Integer, Double> o2)
			{
				return o2.getValue().compareTo(o1.getValue());
			}
		});
		
		// creation d'un tableau trie par poids de noeud decroissant
		for (final Entry<Integer, Double> entry : entries)
			this.sortedNodes.add(this.nodes.get(entry.getKey()));
	}
	
	
	/**
	 * 
	 */
	public void search()
	{
		getDBData();
		computeWeights();
		sortNodes();
		
		for (Node n : this.sortedNodes)
		{
			String ref = docs.get(n.getFdoc()).getName()+":"+n.getXPath();
			//retrieveSection(ref);
		}
	}

	/**
	 * A partir d'un nom de document et d'un xpath, retourne la section
	 * correspondante
	 * 
	 * @param docName
	 * @param path
	 * @return la section
	 *         <p>
	 *         desiree du document correspondant
	 */
	public String retrieveSection(String ref)
	{
		long start = System.currentTimeMillis();
		
		String section = null;
		DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
		domFactory.setNamespaceAware(true);
		DocumentBuilder builder;

		String[] res = ref.split(":");
		String docName = res[0];
		String path = res[1];

		try
		{
			builder = domFactory.newDocumentBuilder();
			Document doc = builder.parse("ressources/Collection/" + docName);

			XPathFactory factory = XPathFactory.newInstance();
			XPath xpath = factory.newXPath();
			XPathExpression expr = xpath.compile(path);

			Object result = expr.evaluate(doc, XPathConstants.NODESET);
			NodeList nodes = (NodeList) result;

			// le resultat est cense etre unique donc on le recupere ici
			section = nodes.item(0).getTextContent();

		} catch (ParserConfigurationException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (SAXException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (XPathExpressionException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

//		RepCo.printD(docName+" "+path, start);
//		System.out.println(section);
		
		return section;
	}
	
	public double evaluation(String qrelFile, int level)
	{
		//System.out.println(qrelFile+" level "+level);
		
		HashMap<Integer, ArrayList<String>> qrel = Utils.parseQrel(qrelFile, this.dc);
		
		int selected = 0;
		int total = 0;
		
		for (ArrayList<String> xpaths: qrel.values())
			total += xpaths.size();
		
		for (int i=0; i<level; i++)
		{
			Node n = sortedNodes.get(i);
			
			if (n != null)
				if (qrel.containsKey(n.getFdoc()))
					if (qrel.get(n.getFdoc()).contains(n.getXPath()))
						selected++;
		}

		// precision = nb doc pertinents selectionnes / level
		double precision = (double) selected / (double) level;
		
		//System.out.println("PRECISION: "+(precision*100)+"% - TOTAL: "+total);
		
		return precision;
	}
	
	public ArrayList<Search> generateCloseSearch()
	{
		ArrayList<Search> closeSearch = new ArrayList<>();
		
		ArrayList<String> req = Utils.parseRequestSimple(this.request);
        
        String newReq;
        SparqlRequest sr = new SparqlRequest();
        
        ArrayList<ArrayList<String>> as = Utils.recCombine(req, sr);
        for (ArrayList<String> a: as)
        {
        	newReq = "";
        	for (String s: a)
        		newReq += s+" ";
        	newReq.substring(0, newReq.length()-1);
        	closeSearch.add(new Search(newReq, 0.5, this.dc));
        }
        
        return closeSearch;
	}
	
	public HashMap<Integer, Double> mergeWeights(ArrayList<Search> searchs)
	{
		this.globalNodeWeight.clear();
		
		ArrayList<Node> nodes = new ArrayList<>();
		
		for (Search s: searchs)
			nodes.addAll(s.nodes.values());

		HashSet hs = new HashSet();
		hs.addAll(nodes);
		nodes.clear();
		nodes.addAll(hs);
		
		for (Node node : nodes)
		{
			int in = 0;
			double weight = 1.;
			
			for (Search s : searchs)
			{
				//for (HashMap<Integer, Double> w : s.nodeWeights)
				{
					if (s.globalNodeWeight.containsKey(node.getID()))
					{
						weight += s.globalNodeWeight.get(node.getID());
						in++;
					}
				}
				
			}
			
			weight *= Math.log(1+in);
			
			this.globalNodeWeight.put(node.getID(), weight);
		}
		
		return globalNodeWeight;
	}
	
	public void useCloseSearch()
	{
		ArrayList<Search> closeSearch = generateCloseSearch();
		mergeWeights(closeSearch);
				
	}

}
